package com.futo.platformplayer.stores.v2

import com.futo.platformplayer.assume
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.states.StateApp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.KSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.serializer
import java.io.File
import java.io.FileNotFoundException
import java.util.UUID
import kotlin.reflect.KClass
import kotlin.reflect.KType

class ManagedStore<T>{
    private val _class: KType;
    private val _name: String;
    private val _dir: File;
    private val _files: HashMap<T, ManagedFile> = hashMapOf();
    private val _serializer: StoreSerializer<T>;

    private val _toReconstruct: ArrayList<ManagedFile> = ArrayList();

    private var _isLoaded = false;

    private var _withBackup: Boolean = true;
    private var _reconstructStore: ReconstructStore<T>? = null;

    private var _withUnique: ((T) -> Any)? = null;

    val className: String? get() = _class.classifier?.assume<KClass<*>>()?.simpleName;

    private var _onModificationCreate: ((T) -> Unit)? = null;
    private var _onModificationDelete: ((T) -> Unit)? = null;

    val name: String;

    constructor(name: String, dir: File, clazz: KType, serializer: StoreSerializer<T>, niceName: String? = null) {
        _name = name;
        this.name = niceName ?: name.let {
            if(it.length > 0)
                return@let it[0].uppercase() + it.substring(1);
            return@let name;
        };
        _serializer = serializer;
        _class = clazz;
        _dir = File(dir, name);
        if(!_dir.exists())
            _dir.mkdir();
    }

    fun withUnique(handler: (T) -> Any): ManagedStore<T> {
        _withUnique = handler;
        return this;
    }
    fun withRestore(backup: ReconstructStore<T>): ManagedStore<T> {
        _reconstructStore = backup;
        return this;
    }
    fun withoutBackup(): ManagedStore<T>{
        _withBackup = false;
        return this;
    }

    fun withOnModified(created: (T)->Unit, deleted: (T)->Unit): ManagedStore<T> {
        _onModificationCreate = created;
        _onModificationDelete = deleted;
        return this;
    }

    fun load(): ManagedStore<T> {
        synchronized(_files) {
            _files.clear();
            val newObjs = _dir.listFiles()?.map { it.nameWithoutExtension }?.distinct()?.toList()?.map { fileId ->
                val mfile = ManagedFile(fileId, _dir);
                val obj = mfile.load(this, _withBackup);
                if(obj == null) {
                    Logger.w(TAG, "Deleting ${logName(mfile.id)}");
                    mfile.delete(false);
                    if(mfile.reconstructFile.exists()) {
                        _toReconstruct.add(mfile);
                        Logger.i(TAG, "Reconstruction required: ${logName(fileId)}");
                    }
                }

                return@map Pair(obj, mfile);
            }?.filter { it.first != null };

            if (newObjs != null) {
                for (obj in newObjs)
                    _files[obj.first!!] = obj.second;
            }
        }
        _isLoaded = true;
        return this;
    }
    fun getMissingReconstructionCount(): Int {
        synchronized(_toReconstruct) {
            return _toReconstruct.size;
        }
    }
    fun hasMissingReconstructions(): Boolean {
        synchronized(_toReconstruct) {
            return _toReconstruct.any();
        }
    }

    fun deleteMissing() {
        synchronized(_toReconstruct) {
            for(file in _toReconstruct)
                file.delete(true);
            _toReconstruct.clear();
        }
    }
    suspend fun importReconstructions(items: List<String>, cache: ImportCache? = null, onProgress: ((Int, Int)->Unit)? = null): ReconstructionResult {
        var successes = 0;
        val exs = ArrayList<Throwable>();

        val total = items.size;
        var finished = 0;

        val builder = ReconstructStore.Builder();

        for (recon in items) {
            onProgress?.invoke(0, total);
            //Retry once
            for (i in 0 .. 1) {
                try {
                    Logger.i(TAG, "Importing ${logName(recon)}");
                    val reconId = createFromReconstruction(recon, builder, cache);
                    successes++;
                    Logger.i(TAG, "Imported ${logName(reconId)}");
                    break;
                } catch (ex: Throwable) {
                    Logger.e(TAG, "Failed to reconstruct import", ex);
                    if (i == 1) {
                        exs.add(ex);
                    }
                }
            }
            finished++;
            onProgress?.invoke(finished, total);
        }
        return ReconstructionResult(successes, exs, builder.messages);
    }
    
    suspend fun reconstructMissing(onProgress: ((Int, Int)->Unit)? = null): ReconstructionResult {
        var successes = 0;
        val exs = ArrayList<Throwable>();
        val missings = synchronized(_toReconstruct) { _toReconstruct.toList(); }

        val total = missings.size;
        var finished = 0;

        val builder = ReconstructStore.Builder();

        for (missing in missings) {
            //Retry once
            for (i in 0 .. 1) {
                try {
                    Logger.i(TAG, "Started reconstructing ${logName(missing.id)}");
                    val reconstructed = missing.reconstruct(this, builder);

                    missing.write(_serializer.serialize(_class, reconstructed), _withBackup);
                    synchronized(_files) {
                        _files.put(reconstructed, missing);
                    }
                    synchronized(_toReconstruct) {
                        _toReconstruct.remove(missing);
                    }
                    successes++;
                    Logger.i(TAG, "Reconstructed ${logName(missing.id)}");
                    break;
                } catch (ex: Throwable) {
                    Logger.e(TAG, "Failed to reconstruct ${logName(missing.id)}", ex);

                    if (i == 1) {
                        exs.add(ex);
                    }
                }
                finished++;
                onProgress?.invoke(finished, total);
            }
        }
        return ReconstructionResult(successes, exs, builder.messages);
    }


    fun count(): Int {
        synchronized(_files) {
            return _files.size;
        }
    }
    fun getItems(): List<T> {
        synchronized(_files) {
            return _files.map { it.key };
        }
    }
    fun queryItem(query: (Iterable<T>)->T?) : T? {
        synchronized(_files) {
            return query(_files.keys.asIterable());
        }
    }
    fun hasItems(): Boolean {
        synchronized(_files) {
            return _files.any();
        }
    }
    fun hasItem(query: (T)-> Boolean): Boolean {
        synchronized(_files) {
            return _files.keys.any { query(it) };
        }
    }
    fun findItem(query: (T)->Boolean): T? {
        synchronized(_files) {
            return _files.keys.find(query);
        }
    }
    fun findItems(query: (T)->Boolean): List<T> {
        synchronized(_files) {
            return _files.keys.filter(query);
        }
    }

    fun getFile(obj: T): ManagedFile? {
        synchronized(_files) {
            if(_files.containsKey(obj))
                return _files[obj];
            return null;
        }
    }


    fun saveAsync(obj: T, withReconstruction: Boolean = false, onlyExisting: Boolean = false) {
        val scope = StateApp.instance.scopeOrNull;
        if(scope != null)
            scope.launch(Dispatchers.IO) {
                try {
                    save(obj, withReconstruction, onlyExisting);
                } catch (e: Throwable) {
                    Logger.e(TAG, "Failed to save.", e);
                }
            };
        else
            save(obj, withReconstruction, onlyExisting);
    }
    fun saveAllAsync(objs: List<T>, withReconstruction: Boolean = false) {
        val scope = StateApp.instance.scopeOrNull;
        if(scope != null)
            scope.launch(Dispatchers.IO) {
                saveAll(objs, withReconstruction);
            };
        else
            saveAll(objs, withReconstruction);
    }
    fun save(obj: T, withReconstruction: Boolean = false, onlyExisting: Boolean = false) {
        synchronized(_files) {
            val uniqueVal = if(_withUnique != null)
                _withUnique!!(obj);
            else null;

            var file = getFile(obj);
            if (file != null) {
                Logger.v(TAG, "Saving file ${logName(file.id)}");
                val encoded = _serializer.serialize(_class, obj);
                file.write(encoded, _withBackup);
                if(_reconstructStore != null && (_reconstructStore!!.backupOnSave || withReconstruction))
                    saveReconstruction(file, obj);
            }
            else if(!onlyExisting && (uniqueVal == null || !_files.any { _withUnique!!(it.key) == uniqueVal })) {
                file = saveNew(obj);
                if(_reconstructStore != null && (_reconstructStore!!.backupOnCreate || withReconstruction))
                    saveReconstruction(file, obj);
                _onModificationCreate?.invoke(obj)
            }
        }
    }
    fun saveAll(items: List<T>, withReconstruction: Boolean = false, onlyExisting: Boolean = false) {
        for(obj in items)
            save(obj, withReconstruction, onlyExisting);
    }

    suspend fun fromReconstruction(reconstruction: String, cache: ImportCache? = null): T {
        if(_reconstructStore == null)
            throw IllegalStateException("Can't reconstruct as no reconstruction is implemented for this type");

        val id = UUID.randomUUID().toString();
        return _reconstructStore!!.toObjectWithHeader(id, reconstruction, ReconstructStore.Builder(), cache);
    }

    suspend fun createFromReconstruction(reconstruction: String, builder: ReconstructStore.Builder, cache: ImportCache? = null): String {
        if(_reconstructStore == null)
            throw IllegalStateException("Can't reconstruct as no reconstruction is implemented for this type");

        val id = UUID.randomUUID().toString();
        val reconstruct = _reconstructStore!!.toObjectWithHeader(id, reconstruction, builder, cache);
        save(reconstruct);
        return id;
    }

    fun delete(item: T) {
        synchronized(_files) {
            val file = _files[item];
            if(file != null) {
                if(item is IStoreItem)
                    item.onDelete();
                _files.remove(item);
                Logger.v(TAG, "Deleting file ${logName(file.id)}");
                file.delete();
                _onModificationDelete?.invoke(item)
            }
        }
    }
    fun deleteAll() {
        synchronized(_files) {
            val keys = _files.keys.toList();
            for(key in keys)
                delete(key);
        }
    }

    private fun saveNew(obj: T): ManagedFile {
        synchronized(_files) {
            val id = UUID.randomUUID().toString();
            Logger.v(TAG, "New file ${logName(id)}");
            val encoded = _serializer.serialize(_class, obj);

            val mfile = ManagedFile(id, _dir);
            mfile.write(encoded, _withBackup);

            _files.put(obj, mfile);
            return mfile;
        }
    }

    fun getAllReconstructionStrings(withHeader: Boolean = false): List<String> {
        if(_reconstructStore == null)
            throw IllegalStateException("Can't reconstruct as no reconstruction is implemented for this type");

        return getItems().map {
            getReconstructionString(it, withHeader)
        };
    }
    fun getReconstructionString(obj: T, withHeader: Boolean = false): String {
        if(_reconstructStore == null)
            throw IllegalStateException("Can't reconstruct as no reconstruction is implemented for this type");

        if(withHeader)
            return _reconstructStore!!.toReconstructionWithHeader(obj, className ?: "");
        else
            return _reconstructStore!!.toReconstruction(obj);
    }
    private fun saveReconstruction(file: ManagedFile, obj: T) {
        if(_reconstructStore == null)
            return;
        val reconstruction = getReconstructionString(obj, true);
        file.writeReconstruction(reconstruction);
    }

    fun isReconstructionIdentifier(identifier: String): Boolean {
        if(_reconstructStore == null)
            throw IllegalStateException("Can't reconstruct as no reconstruction is implemented for this type");

        return identifier == (_reconstructStore?.identifierName ?: className)
    }
    fun isReconstructionHeader(recon: String): Boolean {
        val identifier = getReconstructionIdentifier(recon);
        return identifier != null && isReconstructionIdentifier(identifier);
    }

    class ManagedFile(
        val id: String,
        val dir: File
    ) {
        val file: File = File(dir, id);
        val bakFile: File = File(dir, id + ".bak");
        val reconstructFile: File = File(dir, id + ".rec");

        fun <T> load(store: ManagedStore<T>, withBackup: Boolean = true): T? {
            synchronized(this) {
                try {
                    if(!file.exists())
                        throw FileNotFoundException();
                    val data = read();

                    //Uncomment to test migration
                    //if(className == "Subscription") throw IllegalStateException("Test Exception");

                    return store._serializer.deserialize(store._class, data);
                }
                catch(ex: Throwable) {
                    if(ex !is FileNotFoundException)
                        Logger.w(TAG, "Failed to parse ${store.logName(id)}", ex);

                    if(withBackup) {
                        val backData = readBackup();
                        try {
                            if (backData != null) {
                                Logger.i(TAG, "Loading from backup ${store.logName(id)}");
                                return store._serializer.deserialize(store._class, backData);
                            } else Logger.i(TAG, "No backup exists for ${store.logName(id)}")
                        } catch (bakEx: Throwable) {
                            Logger.w(TAG, "Failed to bakfile parse ${store.logName(id)}", bakEx);
                        }
                    }
                }

                Logger.w(TAG, "No object from ${store.logName(id)}");
                return null;
            }
        }

        suspend fun <T> reconstruct(store: ManagedStore<T>, builder: ReconstructStore.Builder): T {
            if(store._reconstructStore == null)
                throw IllegalStateException("No reconstruction logic exists?");

            val reconstruction = readReconstruction()
                ?: throw FileNotFoundException("No reconstruction found");

            val reconstructed: T;
            try {
                reconstructed = store._reconstructStore!!.toObjectWithHeader(id, reconstruction, builder);
            }
            catch(ex: Throwable) {
                throw ex;
            }
            return reconstructed;
        }


        fun write(data: ByteArray, withBackup: Boolean = true) {
            if(withBackup && file.exists())
                file.copyTo(bakFile, true);
            file.writeBytes(data);
        }
        fun writeReconstruction(str: String) {
            reconstructFile.writeText(str, Charsets.UTF_8);
        }

        fun read(): ByteArray {
            return file.readBytes();
        }
        fun readBackup(): ByteArray? {
            if(bakFile.exists())
                return bakFile.readBytes();
            return null;
        }
        fun readReconstruction(): String? {
            if(reconstructFile.exists())
                return reconstructFile.readText(Charsets.UTF_8);
            return null;
        }

        fun delete(deleteReconstruction: Boolean = true) {
            if(file.exists())
                file.delete();
            if(bakFile.exists())
                bakFile.delete();
            if(deleteReconstruction && reconstructFile.exists())
                reconstructFile.delete();
        }
    }

    data class ReconstructionResult(
        val success: Int = 0,
        val exceptions: List<Throwable>,
        val messages: List<String>
    );

    private fun logName(id: String?): String {
        return "${_name}:[${(_class.classifier as KClass<*>).simpleName}] ${id ?: ""}";
    }

    companion object {
        val TAG = "ManagedStore";
        val RECONSTRUCTION_HEADER_OPERATOR = "@/";

        fun getReconstructionIdentifier(recon: String): String? {
            if(!recon.startsWith(RECONSTRUCTION_HEADER_OPERATOR) || !recon.contains("\n"))
                return null;
            else
                return recon.substring(2, recon.indexOf("\n"));
        }
    }
}

interface StoreSerializer<T> {
    fun serialize(clazz: KType, obj: T): ByteArray;
    fun deserialize(clazz: KType, obj: ByteArray): T;
}

class JsonStoreSerializer<T>: StoreSerializer<T> {
    private val _serializer: KSerializer<T>
    val jsonSer = Json { ignoreUnknownKeys = true; encodeDefaults = true; }

    constructor(serializer: KSerializer<T>) {
        _serializer = serializer;
    }

    override fun serialize(clazz: KType, obj: T): ByteArray {
        val json = jsonSer.encodeToString<T>(_serializer,obj)//gson.toJson(obj);
        return json.toByteArray(Charsets.UTF_8);
    }

    override fun deserialize(clazz: KType, obj: ByteArray): T {
        val json = String(obj, Charsets.UTF_8);
        try {
            return jsonSer.decodeFromString<T>(_serializer, json);
        }
        catch(ex: Throwable) {
            Logger.e(ManagedStore.TAG, "Json for ${(clazz.classifier as KClass<*>).simpleName}:\n" + json, ex);
            throw ex;
        }
    }

    companion object {
        inline fun <reified T> create(serializer: KSerializer<T>? = null): JsonStoreSerializer<T> {
            return JsonStoreSerializer<T>(if(serializer != null) serializer else serializer<T>());
        }
    }
}